#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import time
import glob
import shutil
import hashlib
import pycdlib
import zipfile
import unrardll
import subprocess
import mirrors.plugin
from dataclasses import dataclass
from libtorrent import session as torrent_session
from libtorrent import torrent_info
from libtorrent import torrent_handle


class Main:

    def __init__(self):
        self.cfg = mirrors.plugin.params["config"]
        self.dataDir = mirrors.plugin.params["storage-file"]["data-directory"]

        self.downloadingDir = os.path.join(self.dataDir, ".downloading")
        self.seedingDir = os.path.join(self.dataDir, ".seeding")

        self.webFileMap = {}
        self.torrentFileMap = {}
        self.funcFileMap = {}

        # win98
        for k in Win98.winIsoUrlDict.keys():
            self.funcFileMap.update({
                k: ("win98", None, Win98.downloadWin98),
            })
        self.funcFileMap.update({
            "Windows_98_Second_Edition_Boot.ima": ("bfloppy98", 1474560, Win98.downloadBootFloppy98)
        })
        self.webFileMap.update({
            "U98SESP3.EXE": {                                                   # unofficial win98se service pack 3.66, https://www.htasoft.com/u98sesp/
                "urls": [
                    "http://www.techtalk.cc/download/U98SESP3.EXE",
                ],
                "size": 126378496,
                "sha256": "54c775351f914efb1cf09bd800e093589fca471eeb59107d71c61ff6c86ff6ef",
            },
        })
        self.funcFileMap.update({                                               # win98 driver pack, https://msfn.org/board/topic/98369-driver-packs-for-windows-959898seme/
            "DP_w98_1.01.zip": ("dp98", None, Win98.downloadDp98),
            "DP_w98_2.01.zip": ("dp98", None, Win98.downloadDp98),
            "DP_w98_3.01.zip": ("dp98", None, Win98.downloadDp98),
            "DP_w98_4.02.zip": ("dp98", None, Win98.downloadDp98),
            "DP_w98_5.01.zip": ("dp98", None, Win98.downloadDp98),
            "DP_w98_6.01.zip": ("dp98", None, Win98.downloadDp98),
            "DP_w98_7.01.zip": ("dp98", None, Win98.downloadDp98),
        })

        # office for win98
        for k in Win98.officeIsoUrlDict.keys():
            self.funcFileMap.update({
                k: ("office97", None, Win98.downloadOffice97),
            })

        # winxp
        self.torrentFileMap.update({
            "windows-xp-all-sp-msdn-iso-files-en-de-ru-tr-x86-x64.torrent": {
                "en_windows_xp_home_with_service_pack_3_x86_cd_x14-92413.iso": None,
                "en_windows_xp_professional_with_service_pack_3_x86_cd_x14-80428.iso": None,
                "tr_windows_xp_professional_with_service_pack_3_x86_cd_x14-80496.iso": None,
                "de_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-73985.iso": None,
                "en_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-73974.iso": None,
                "ru_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74146.iso": None,
                "tr_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74085.iso": None,
                "en_win_xp_pro_x64_with_sp2_vl_x13-41611.iso": None,
                "en_win_xp_tabletpc_2005_disc1_vl.iso": None,
                "en_win_xp_tabletpc_2005_disc2_vl.iso": None,
            },
            "DP_CPU_wnt5_x86-32_1005.torrent": {
                "DP_CPU_wnt5_x86-32_1005.7z": None,
            },
            "DP_Chipset_wnt5_x86-32_1209.torrent": {
                "DP_Chipset_wnt5_x86-32_1209.7z": None,
            },
            "DP_MassStorage_wnt5_x86-32_1209.torrent": {
                "DP_MassStorage_wnt5_x86-32_1209.7z": None,
            },
            "DP_Runtimes_wnt5_x86-32_1206.torrent": {
                "DP_Runtimes_wnt5_x86-32_1206.7z": None,
            },
            "DP_Graphics_A_wnt5_x86-32_1206.torrent": {
                "DP_Graphics_A_wnt5_x86-32_1206.7z": None,
            },
            "DP_Graphics_B_wnt5_x86-32_1206.torrent": {
                "DP_Graphics_B_wnt5_x86-32_1206.7z": None,
            },
            "DP_Graphics_C_wnt5_x86-32_1206.torrent": {
                "DP_Graphics_C_wnt5_x86-32_1206.7z": None,
            },
            "DP_Graphics_Languages_wnt5_x86-32_1206.torrent": {
                "DP_Graphics_Languages_wnt5_x86-32_1206.7z": None,
            },
            "DP_Graphics_PhysX_wnt5_x86-32_1206.torrent": {
                "DP_Graphics_PhysX_wnt5_x86-32_1206.7z": None,
            },
            "DP_Monitor_wnt5_x86-32_1005.torrent": {
                "DP_Monitor_wnt5_x86-32_1005.7z": None,
            },
            "DP_Sound_A_wnt5_x86-32_1111.torrent": {
                "DP_Sound_A_wnt5_x86-32_1111.7z": None,
            },
            "DP_Sound_B_wnt5_x86-32_1111.torrent": {
                "DP_Sound_B_wnt5_x86-32_1111.7z": None,
            },
            "DP_LAN_wnt5_x86-32_1205.torrent": {
                "DP_LAN_wnt5_x86-32_1205.7z": None,
            },
            "DP_LAN-RIS_wnt5_x86-32_1011.torrent": {
                "DP_LAN-RIS_wnt5_x86-32_1011.7z": None,
            },
            "DP_Modem_wnt5_x86-32_1205.torrent": {
                "DP_Modem_wnt5_x86-32_1205.7z": None,
            },
            "DP_WLAN_wnt5_x86-32_1202.torrent": {
                "DP_WLAN_wnt5_x86-32_1202.7z": None,
            },
            "DP_HID_wnt5_x86-32_1209.torrent": {
                "DP_HID_wnt5_x86-32_1209.7z": None,
            },
        })

        # win7
        self.torrentFileMap["Microsoft_SYSTEM.torrent"] = {}
        self.torrentFileMap["Microsoft_SYSTEM.torrent"].update({                                    # windows 7 enterprise
            k: {
                "in-dir": "{1}【Win7_MSDN】/Windows 7 Enterprise",
            } for k in [
                "en_windows_7_enterprise_n_with_sp1_x64_dvd_u_677704.iso",
                "en_windows_7_enterprise_n_with_sp1_x86_dvd_u_677703.iso",
                "en_windows_7_enterprise_n_x64_dvd_x16-11943.iso",
                "en_windows_7_enterprise_n_x86_dvd_x16-11935.iso",
                "en_windows_7_enterprise_with_sp1_x64_dvd_620201.iso",
                "en_windows_7_enterprise_with_sp1_x64_dvd_u_677651.iso",
                "en_windows_7_enterprise_with_sp1_x86_dvd_620186.iso",
                "en_windows_7_enterprise_with_sp1_x86_dvd_u_677710.iso",
                "en_windows_7_enterprise_x64_dvd_X15-70749.iso",
                "en_windows_7_enterprise_x86_dvd_X15-70745.iso",
                "cn_windows_7_enterprise_with_sp1_x64_dvd_620654.iso",
                "cn_windows_7_enterprise_with_sp1_x64_dvd_u_677685.iso",
                "cn_windows_7_enterprise_with_sp1_x86_dvd_620153.iso",
                "cn_windows_7_enterprise_with_sp1_x86_dvd_u_677716.iso",
                "cn_windows_7_enterprise_x64_dvd_x15-70741.iso",
                "cn_windows_7_enterprise_x86_dvd_x15-70737.iso",
                "hk_windows_7_enterprise_with_sp1_x64_dvd_620688.iso",
                "hk_windows_7_enterprise_with_sp1_x64_dvd_u_677687.iso",
                "hk_windows_7_enterprise_with_sp1_x86_dvd_620147.iso",
                "hk_windows_7_enterprise_with_sp1_x86_dvd_u_677720.iso",
                "hk_windows_7_enterprise_x64_dvd_x15-70981.iso",
                "hk_windows_7_enterprise_x86_dvd_x15-70977.iso",
                "tw_windows_7_enterprise_with_sp1_x64_dvd_620173.iso",
                "tw_windows_7_enterprise_with_sp1_x64_dvd_u_677689.iso",
                "tw_windows_7_enterprise_with_sp1_x86_dvd_620158.iso",
                "tw_windows_7_enterprise_with_sp1_x86_dvd_u_677723.iso",
                "tw_windows_7_enterprise_x64_dvd_x15-70973.iso",
                "tw_windows_7_enterprise_x86_dvd_x15-70969.iso",
            ]
        })
        self.torrentFileMap.update({                                                                # windows 7 enterprise
            "en_windows_7_enterprise_n_with_sp1_x64_dvd_620165.iso.torrent": {
                "en_windows_7_enterprise_n_with_sp1_x64_dvd_620165.iso": None,
            },
            "en_windows_7_enterprise_n_with_sp1_x86_dvd_620372.iso.torrent": {
                "en_windows_7_enterprise_n_with_sp1_x86_dvd_620372.iso": None,
            },
        })
        self.torrentFileMap["Microsoft_SYSTEM.torrent"].update({                                    # windows 7 home
            k: {
                "in-dir": "{1}【Win7_MSDN】/Windows 7 Home",
            } for k in [
                "en_windows_7_home_basic_with_sp1_x86_dvd_620759.iso",
                "en_windows_7_home_basic_with_sp1_x86_dvd_u_676470.iso",
                "en_windows_7_home_basic_x86_dvd_x15-65654.iso",
                "en_windows_7_home_premium_n_with_sp1_x64_dvd_u_676825.iso",
                "en_windows_7_home_premium_n_with_sp1_x86_dvd_u_676913.iso",
                "en_windows_7_home_premium_n_x64_dvd_x16-13459.iso",
                "en_windows_7_home_premium_n_x86_dvd_x16-13457.iso",
                "en_windows_7_home_premium_with_sp1_x64_dvd_620851.iso",
                "en_windows_7_home_premium_with_sp1_x64_dvd_u_676549.iso",
                "en_windows_7_home_premium_with_sp1_x86_dvd_620895.iso",
                "en_windows_7_home_premium_with_sp1_x86_dvd_u_676701.iso",
                "en_windows_7_home_premium_x64_dvd_x15-65733.iso",
                "en_windows_7_home_premium_x86_dvd_x15-65732.iso",
                "cn_windows_7_home_basic_with_sp1_x86_dvd_620743.iso",
                "cn_windows_7_home_basic_with_sp1_x86_dvd_u_676500.iso",
                "cn_windows_7_home_basic_x86_dvd_x15-65975.iso",
                "cn_windows_7_home_premium_with_sp1_x64_dvd_620792.iso",
                "cn_windows_7_home_premium_with_sp1_x64_dvd_u_676691.iso",
                "cn_windows_7_home_premium_with_sp1_x86_dvd_620856.iso",
                "cn_windows_7_home_premium_with_sp1_x86_dvd_u_676770.iso",
                "cn_windows_7_home_premium_x64_dvd_x15-65718.iso",
                "cn_windows_7_home_premium_x86_dvd_x15-65717.iso",
                "hk_windows_7_home_basic_with_sp1_x86_dvd_624181.iso",
                "hk_windows_7_home_basic_with_sp1_x86_dvd_u_676504.iso",
                "hk_windows_7_home_basic_x86_dvd_x15-65644.iso",
                "hk_windows_7_home_premium_with_sp1_x64_dvd_620781.iso",
                "hk_windows_7_home_premium_with_sp1_x64_dvd_u_676697.iso",
                "hk_windows_7_home_premium_with_sp1_x86_dvd_620848.iso",
                "hk_windows_7_home_premium_with_sp1_x86_dvd_u_676771.iso",
                "hk_windows_7_home_premium_x64_dvd_x15-65722.iso",
                "hk_windows_7_home_premium_x86_dvd_x15-65721.iso",
                "tw_windows_7_home_basic_with_sp1_x86_dvd_624173.iso",
                "tw_windows_7_home_basic_with_sp1_x86_dvd_u_676507.iso",
                "tw_windows_7_home_basic_x86_dvd_x15-65642.iso",
                "tw_windows_7_home_premium_with_sp1_x64_dvd_620802.iso",
                "tw_windows_7_home_premium_with_sp1_x64_dvd_u_676703.iso",
                "tw_windows_7_home_premium_with_sp1_x86_dvd_620862.iso",
                "tw_windows_7_home_premium_with_sp1_x86_dvd_u_676772.iso",
                "tw_windows_7_home_premium_x64_dvd_x15-65720.iso",
                "tw_windows_7_home_premium_x86_dvd_x15-65719.iso",
            ]
        })
        self.torrentFileMap["Microsoft_SYSTEM.torrent"].update({                                    # windows 7 professional
            k: {
                "in-dir": "{1}【Win7_MSDN】/Windows 7 Professional",
            } for k in [
                "en_windows_7_professional_n_with_sp1_x64_dvd_u_677207.iso",
                "en_windows_7_professional_n_with_sp1_x86_dvd_u_677328.iso",
                "en_windows_7_professional_n_x64_dvd_x16-13550.iso",
                "en_windows_7_professional_n_x86_dvd_x16-13548.iso",
                "en_windows_7_professional_vl_build_x64_dvd_x15-71037.iso",
                "en_windows_7_professional_vl_build_x86_dvd_x15-71033.iso",
                "en_windows_7_professional_with_sp1_vl_build_x64_dvd_622458.iso",
                "en_windows_7_professional_with_sp1_vl_build_x64_dvd_u_677791.iso",
                "en_windows_7_professional_with_sp1_vl_build_x86_dvd_623530.iso",
                "en_windows_7_professional_with_sp1_vl_build_x86_dvd_u_677896.iso",
                "en_windows_7_professional_with_sp1_x64_dvd_621750.iso",
                "en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso",
                "en_windows_7_professional_with_sp1_x86_dvd_631025.iso",
                "en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso",
                "en_windows_7_professional_x64_dvd_x15-65805.iso",
                "en_windows_7_professional_x86_dvd_x15-65804.iso",
                "cn_windows_7_professional_vl_build_x64_dvd_x15-71029.iso",
                "cn_windows_7_professional_vl_build_x86_dvd_x15-71025.iso",
                "cn_windows_7_professional_with_sp1_vl_build_x64_dvd_622431.iso",
                "cn_windows_7_professional_with_sp1_vl_build_x64_dvd_u_677816.iso",
                "cn_windows_7_professional_with_sp1_vl_build_x86_dvd_623494.iso",
                "cn_windows_7_professional_with_sp1_vl_build_x86_dvd_u_677939.iso",
                "cn_windows_7_professional_with_sp1_x64_dvd_621744.iso",
                "cn_windows_7_professional_with_sp1_x64_dvd_u_677031.iso",
                "cn_windows_7_professional_with_sp1_x86_dvd_622569.iso",
                "cn_windows_7_professional_with_sp1_x86_dvd_u_677162.iso",
                "cn_windows_7_professional_x64_dvd_x15-65791.iso",
                "cn_windows_7_professional_x86_dvd_x15-65790.iso",
                "hk_windows_7_professional_vl_build_x64_dvd_x15-71268.iso",
                "hk_windows_7_professional_vl_build_x86_dvd_x15-71264.iso",
                "hk_windows_7_professional_with_sp1_vl_build_x64_dvd_622426.iso",
                "hk_windows_7_professional_with_sp1_vl_build_x64_dvd_u_677823.iso",
                "hk_windows_7_professional_with_sp1_vl_build_x86_dvd_623490.iso",
                "hk_windows_7_professional_with_sp1_vl_build_x86_dvd_u_677951.iso",
                "hk_windows_7_professional_with_sp1_x64_dvd_621743.iso",
                "hk_windows_7_professional_with_sp1_x64_dvd_u_677038.iso",
                "hk_windows_7_professional_with_sp1_x86_dvd_622563.iso",
                "hk_windows_7_professional_with_sp1_x86_dvd_u_677177.iso",
                "hk_windows_7_professional_x86_dvd_x15-65794.iso",
                "tw_windows_7_professional_vl_build_x64_dvd_x15-71260.iso",
                "tw_windows_7_professional_vl_build_x86_dvd_x15-71256.iso",
                "tw_windows_7_professional_with_sp1_vl_build_x64_dvd_622435.iso",
                "tw_windows_7_professional_with_sp1_vl_build_x64_dvd_u_677829.iso",
                "tw_windows_7_professional_with_sp1_vl_build_x86_dvd_623497.iso",
                "tw_windows_7_professional_with_sp1_vl_build_x86_dvd_u_677771.iso",
                "tw_windows_7_professional_with_sp1_x64_dvd_621745.iso",
                "tw_windows_7_professional_with_sp1_x64_dvd_u_677047.iso",
                "tw_windows_7_professional_with_sp1_x86_dvd_622573.iso",
                "tw_windows_7_professional_with_sp1_x86_dvd_u_677189.iso",
                "tw_windows_7_professional_x64_dvd_x15-65793.iso",
                "tw_windows_7_professional_x86_dvd_x15-65792.iso",
            ]
        })
        self.torrentFileMap["Microsoft_SYSTEM.torrent"].update({                                    # windows 7 starter
            k: {
                "in-dir": "{1}【Win7_MSDN】/Windows 7 Starter",
            } for k in [
                "en_windows_7_starter_n_with_sp1_x86_dvd_u_678339.iso",
                "en_windows_7_starter_with_sp1_x86_dvd_624016.iso",
                "en_windows_7_starter_with_sp1_x86_dvd_u_678562.iso",
                "cn_windows_7_starter_with_sp1_x86_dvd_624050.iso",
                "cn_windows_7_starter_with_sp1_x86_dvd_u_678536.iso",
                "hk_windows_7_starter_with_sp1_x86_dvd_624041.iso",
                "hk_windows_7_starter_with_sp1_x86_dvd_u_678543.iso",
                "tw_windows_7_starter_with_sp1_x86_dvd_624053.iso",
                "tw_windows_7_starter_with_sp1_x86_dvd_u_678549.iso",
            ]
        })
        self.torrentFileMap["Microsoft_SYSTEM.torrent"].update({                                    # windows 7 ultimate
            k: {
                "in-dir": "{1}【Win7_MSDN】/Windows 7 Ultimate",
            } for k in [
                "en_windows_7_ultimate_n_with_sp1_x64_dvd_u_677543.iso",
                "en_windows_7_ultimate_n_with_sp1_x86_dvd_u_677597.iso",
                "en_windows_7_ultimate_n_x64_dvd_x16-13641.iso",
                "en_windows_7_ultimate_n_x86_dvd_x16-13640.iso",
                "en_windows_7_ultimate_with_sp1_x64_dvd_618240.iso",
                "en_windows_7_ultimate_with_sp1_x64_dvd_u_677332.iso",
                "en_windows_7_ultimate_with_sp1_x86_dvd_619077.iso",
                "en_windows_7_ultimate_with_sp1_x86_dvd_u_677460.iso",
                "en_windows_7_ultimate_x64_dvd_x15-65922.iso",
                "en_windows_7_ultimate_x86_dvd_x15-65921.iso",
                "cn_windows_7_ultimate_with_sp1_x64_dvd_618537.iso",
                "cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso",
                "cn_windows_7_ultimate_with_sp1_x86_dvd_618763.iso",
                "cn_windows_7_ultimate_with_sp1_x86_dvd_u_677486.iso",
                "cn_windows_7_ultimate_x64_dvd_x15-66043.iso",
                "cn_windows_7_ultimate_x86_dvd_x15-65907.iso",
                "hk_windows_7_ultimate_with_sp1_x64_dvd_618544.iso",
                "hk_windows_7_ultimate_with_sp1_x64_dvd_u_677411.iso",
                "hk_windows_7_ultimate_with_sp1_x86_dvd_618761.iso",
                "hk_windows_7_ultimate_with_sp1_x86_dvd_u_677487.iso",
                "hk_windows_7_ultimate_x64_dvd_x15-65911.iso",
                "hk_windows_7_ultimate_x86_dvd_x15-65912.iso",
                "tw_windows_7_ultimate_with_sp1_x64_dvd_618550.iso",
                "tw_windows_7_ultimate_with_sp1_x64_dvd_u_677414.iso",
                "tw_windows_7_ultimate_with_sp1_x86_dvd_618764.iso",
                "tw_windows_7_ultimate_with_sp1_x86_dvd_u_677488.iso",
                "tw_windows_7_ultimate_x64_dvd_x15-65909.iso",
                "tw_windows_7_ultimate_x86_dvd_x15-65908.iso",
            ]
        })
        self.torrentFileMap.update({                                                                # windows 7 ultimate
            "en_windows_7_ultimate_n_with_sp1_x86_dvd_620065.iso.torrent": {
                "en_windows_7_ultimate_n_with_sp1_x86_dvd_620065.iso": None,
            },
        })

        # win10
        self.webFileMap.update({
            "Win10_22H2_English_x32v1.iso": {                                                       # windows 10 official all-in-one iso, from https://www.windowslatest.com/2025/08/08/download-windows-10-iso-version-22h2-before-end-of-support/
                "urls": [
                    "https://software.download.prss.microsoft.com/dbazure/Win10_22H2_English_x32v1.iso?t=5f47d2ef-6480-4168-a3b2-e9fb84e76399&P1=1757541649&P2=601&P3=2&P4=QFP9BgQ3R10MxyuWYizbmxZbdHwsypq6biLtocU%2b2aroz3ROw6eWpSfmRKW%2fRqIc4dkrTDQsoYjX1cOYpmxzsNUQ2Trx9awkKEsnPVPyEIH9B6HPZuADLnRn17vBv5lve11GXF0pCeHXXWSs3bFFKGj%2fy%2b49fFRBb%2b9hfwro826heNqewzyyTvwm0YF5cu1zAWvmIG%2b28uM48LSIKa6HI81m%2fVhYKXMkUTLsKBXf9Du37yATFI3gKMh1H80vp2f5yXHIWf8Vru3Y97ADLQxgXovEBNMh8gJrCnI8iTgHMSez8PSbhSZY1CFAbqvA6Zq5TL2KAp5Aw%2bjSxI5vIZM7aA%3d%3d",
                ],
                "size": 4308279296,
                "sha256": "ac0b7045b6c3a72a4d46daab0944e109a55d9ede3a11b775fdb57c2dd3fca2ef",
            },
            "Win10_22H2_English_x64v1.iso": {
                "urls": [
                    "https://software.download.prss.microsoft.com/dbazure/Win10_22H2_English_x64v1.iso?t=5f47d2ef-6480-4168-a3b2-e9fb84e76399&P1=1757541650&P2=601&P3=2&P4=WiVtfDGSLuNEJdoG8umT%2fnFihjVUjloGoJhZlSP%2bP65ASaXyUbkXaa6GeIz2aAcYrY3LlyOT3Ga4M3HO3bBmwI8plaN18OycCaHrGLx7pHHZ56WOdxR94weV4%2beSzEupyta44T22pojAy3X4ysEq8JbKI6Qpgwzw2HlrX8Hf%2bvfzDlhlzfUW7i0sgmeAXl6RM1CtgSg%2beIahScWELlqca%2bdvoIhSDNrlTRsoFOlbQ5EDOtxYmXN9WyfmMGl6puvttS1BPnkl7pf451TJsMmKXHHuH62yvgqy%2f3o%2b%2ffm8OmKi5xogtlsgKuo%2fOvPWJCbYNxOa8WpsJlZxy0MJJlx3Ow%3d%3d",
                ],
                "size": 6140975104,
                "sha256": "a6f470ca6d331eb353b815c043e327a347f594f37ff525f17764738fe812852e",
            }
        })

    def run(self):
        selfDir = os.path.dirname(os.path.realpath(__file__))

        os.makedirs(self.downloadingDir, exist_ok=True)
        os.makedirs(self.seedingDir, exist_ok=True)

        self._removeRedundantFiles()

        # download web files
        for fn, info in self.webFileMap.items():
            Util.download(fn, info["urls"], os.path.join(self.dataDir, fn), info["size"], info["sha256"])

        # download torrent files
        if True:
            record = []

            downloader = TorrentDownloader()
            try:
                # prepare tmpDir for each torrent file in selfDir, add torrent file into downloader
                for torrentFn in self.torrentFileMap.keys():
                    torrentFullfn = os.path.join(selfDir, torrentFn)
                    seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                    if os.path.exists(seedingParam.resultDirFullfn):
                        # download completed
                        continue
                    downloadParam = self._getDownloadingParamsByTorrentFullfn(torrentFullfn)
                    downloader.addTorrent(torrentFn, torrentFullfn, None, downloadParam.resultDirFullfn)
                    record.append(torrentFn)

                # download, process progress and completed files
                print("Start downloading %d torrents..." % (len(record)))
                downloader.startDownload()
                while True:
                    ret = downloader.pollCompleteTorrent()
                    if ret is None:
                        break

                    if ret[0]:
                        # some torrents are complete
                        for torrentFn in ret[1]:
                            torrentFullfn = os.path.join(selfDir, torrentFn)
                            downloadingParam = self._getDownloadingParamsByTorrentFullfn(torrentFullfn)
                            seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                            Util.forceMove(downloadingParam.resultDirFullfn, seedingParam.resultDirFullfn)

                        print("Download Complete:")
                        for name in ret[1]:
                            print("    %s" % (name))
                    else:
                        # no torrent is complete
                        print("Progress:")
                        for torrentFn, s in ret[1].items():
                            print('    %s (%.2f%%) %.2f MB/s peers: %d' % (torrentFn, s.progress * 100, s.download_rate / 1000000, s.num_peers))

                        # wait for 5 minute before checking status next time, won't affect torrent downloading becaure it is in other threads
                        time.sleep(5 * 60)
            finally:
                downloader.dispose()

            # create symlinks
            for torrentFn, fileDict in self.torrentFileMap.items():
                torrentFullfn = os.path.join(selfDir, torrentFn)
                seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                for fn, fnInfo in fileDict.items():
                    fullfn = os.path.join(self.dataDir, fn)
                    if not os.path.exists(fullfn):
                        if fnInfo is None:
                            symlinkTarget = os.path.join(seedingParam.symlinkTarget, fn)
                            if not os.path.exists(os.path.join(self.dataDir, symlinkTarget)):
                                raise Exception("%s does not exist" % (os.path.join(self.dataDir, symlinkTarget)))
                            os.symlink(symlinkTarget, fullfn)
                        elif isinstance(fnInfo, dict) and "in-dir" in fnInfo:
                            symlinkTarget = os.path.join(seedingParam.symlinkTarget, os.path.join(fnInfo["in-dir"], fn))
                            if not os.path.exists(os.path.join(self.dataDir, symlinkTarget)):
                                raise Exception("%s does not exist" % (os.path.join(self.dataDir, symlinkTarget)))
                            os.symlink(symlinkTarget, fullfn)
                        else:
                            assert False

            # do deduplication for all changed categories
            print("Start deduplication...")
            if len(record) > 0:
                targetDirList = [self._getSeedingParamsByTorrentFullfn(os.path.join(selfDir, x)).resultDirFullfn for x in self.torrentFileMap.keys()]
                self._deduplicateDirList(targetDirList)

        # download by func
        for fn, info in self.funcFileMap.items():
            downloadDir, filesize, func = info
            if Util.validateFile(os.path.join(self.dataDir, fn), filesize):
                continue

            downloadDir = os.path.join(self.downloadingDir, downloadDir)
            os.makedirs(downloadDir, exist_ok=True)
            with TempChdir(downloadDir):
                func(self.dataDir, fn, info)
            Util.forceDelete(downloadDir)

        print("All done.")

    def _removeRedundantFiles(self):
        selfDir = os.path.dirname(os.path.realpath(__file__))

        # process self.seedingDir and self.dataDir
        if True:
            validFiles = []

            # get valid files from self.webFileMap
            for fn in self.webFileMap.keys():
                fullfn = os.path.join(self.dataDir, fn)
                validFiles.append(fullfn)

            # get valid files from self.torrentFileMap
            for torrentFn, fnList in self.torrentFileMap.items():
                torrentFullfn = os.path.join(selfDir, torrentFn)
                seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                if os.path.isdir(seedingParam.resultDirFullfn):
                    validFiles.append(seedingParam.resultDirFullfn)
                for fn in fnList:
                    fullfn = os.path.join(self.dataDir, fn)
                    if os.path.islink(fullfn) and os.readlink(fullfn) == os.path.join(seedingParam.symlinkTarget, fn):
                        validFiles.append(fullfn)

            # get valid files from self.funcFileMap
            for fn in self.funcFileMap.keys():
                fullfn = os.path.join(self.dataDir, fn)
                validFiles.append(fullfn)

            # remove redundant files
            for fullfn in glob.glob(os.path.join(self.dataDir, "*")):
                if fullfn != self.seedingDir and fullfn != self.downloadingDir and fullfn not in validFiles:
                    Util.forceDelete(fullfn)
            for fullfn in glob.glob(os.path.join(self.seedingDir, "*")):
                if fullfn not in validFiles:
                    Util.forceDelete(fullfn)

        # process self.downloadingDir
        if True:
            # get valid files from self.torrentFileMap
            validFiles = []
            for torrentFn in self.torrentFileMap.keys():
                torrentFullfn = os.path.join(selfDir, torrentFn)
                seedingParam = self._getSeedingParamsByTorrentFullfn(torrentFullfn)
                downloadingParam = self._getDownloadingParamsByTorrentFullfn(torrentFullfn)
                if not os.path.isdir(seedingParam.resultDirFullfn) and os.path.isdir(downloadingParam.resultDirFullfn):
                    validFiles.append(downloadingParam.resultDirFullfn)

            # get valid files from self.funcFileMap
            tdict = {}
            for fn, v in self.funcFileMap.items():
                if v not in tdict:
                    tdict[v] = []
                tdict[v].append(os.path.join(self.dataDir, fn))
            for v, fullfnList in tdict.items():
                if not all([os.path.exists(x) for x in fullfnList]):
                    validFiles.append(os.path.join(self.downloadingDir, v[0]))

            # remove redundant files
            for fullfn in glob.glob(os.path.join(self.downloadingDir, "*")):
                if fullfn not in validFiles:
                    Util.forceDelete(fullfn)

    def _getDownloadingParamsByTorrentFullfn(self, torrentFullfn):
        torrentFn = os.path.basename(torrentFullfn)
        return DownloadingParams(
            os.path.join(self.downloadingDir, torrentFn.replace(".torrent", "")),               # download dir full path
        )

    def _getSeedingParamsByTorrentFullfn(self, torrentFullfn):
        torrentFn = os.path.basename(torrentFullfn)
        name = torrentFn.replace(".torrent", "")
        ti = torrent_info(torrentFullfn)
        if ti.num_files() == 1:
            # single file torrent have no inner directory
            symlinkTarget = os.path.join(os.path.basename(self.seedingDir), name)
        else:
            symlinkTarget = os.path.join(os.path.basename(self.seedingDir), name, torrent_info.name(ti))
        return SeedingParams(
            os.path.join(self.seedingDir, name),      # full path of directory for bittorrent
            symlinkTarget,                            # target of symlink for httpdir/ftp
        )

    def _deduplicateDirList(self, dirList):
        # FIXME
        pass


@dataclass
class DownloadingParams:
    resultDirFullfn: str


@dataclass
class SeedingParams:
    resultDirFullfn: str
    symlinkTarget: str


class Win98:

    winIsoUrlDict = {
        "windows-98-se.en-us.iso": [
            "https://winworldpc.com/download/c3b4c382-3210-e280-9d7b-11c3a4c2ac5a/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c3b4c382-3210-e280-9d7b-11c3a4c2ac5a/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se.es.iso": [
            "https://winworldpc.com/download/3fc38bc3-8ee2-8098-18c3-9a11c3a4efbf/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/3fc38bc3-8ee2-8098-18c3-9a11c3a4efbf/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se.it.iso": [
            "https://winworldpc.com/download/23e280a2-c381-c593-43c3-bd11c3a7c29d/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/23e280a2-c381-c593-43c3-bd11c3a7c29d/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se.ja.iso": [
            "https://winworldpc.com/download/378422d1-d4a6-11e7-a73f-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/378422d1-d4a6-11e7-a73f-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se.ko.iso": [
            "https://winworldpc.com/download/e280ba1e-c3a5-3861-5d11-c3a4c2a90f70/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/e280ba1e-c3a5-3861-5d11-c3a4c2a90f70/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        # google-chrome reports virus in it
        # "windows-98-se.ru.iso": [
        #     "https://winworldpc.com/download/3fc3a90f-c2ac-18c3-9a11-c3a4e284a2ef/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
        #     "https://winworldpc.com/download/3fc3a90f-c2ac-18c3-9a11-c3a4e284a2ef/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        # ],
        "windows-98-se.sl.iso": [
            "https://winworldpc.com/download/4008c399-3e18-c39a-11c3-a4e284a2c3a5/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/4008c399-3e18-c39a-11c3-a4e284a2c3a5/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se.tr.iso": [
            "https://winworldpc.com/download/3fc5b84e-3718-c39a-11c3-a4e284a2c3a5/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/3fc5b84e-3718-c39a-11c3-a4e284a2c3a5/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.pt-pt.iso": [
            "https://winworldpc.com/download/e2809348-4e01-2835-11c3-a5e280ba0aef/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/e2809348-4e01-2835-11c3-a5e280ba0aef/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.cz.iso": [
            "https://winworldpc.com/download/c3a35c2a-c3b8-c385-c3b0-11c3a6c2bb2a/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c3a35c2a-c3b8-c385-c3b0-11c3a6c2bb2a/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.de.iso": [
            "https://winworldpc.com/download/c5bee280-b9c2-9de2-809e-1dc2ae11c3a7/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c5bee280-b9c2-9de2-809e-1dc2ae11c3a7/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.en-us.iso": [
            "https://winworldpc.com/download/417d71c2-ae18-c39a-11c3-a4e284a2c3a5/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/417d71c2-ae18-c39a-11c3-a4e284a2c3a5/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.fi.iso": [
            "https://winworldpc.com/download/5fde645b-676e-11e8-a48f-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/5fde645b-676e-11e8-a48f-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.fr.iso": [
            "https://winworldpc.com/download/7e1cfbe2-82e0-11ed-aca0-0200008a0da4/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.gr.iso": [
            "https://winworldpc.com/download/2d0583e4-1f1d-11eb-a665-0200008a0da4/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.hu.iso": [
            "https://winworldpc.com/download/313ec2ab-6c79-c2b0-11c3-a6e280947e52/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/313ec2ab-6c79-c2b0-11c3-a6e280947e52/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.nb.iso": [
            "https://winworldpc.com/download/c3a1c38d-c38b-c397-444e-11c3a6e28094/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c3a1c38d-c38b-c397-444e-11c3a6e28094/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.nl.iso": [
            "https://winworldpc.com/download/c387c3bd-c5b8-c2b6-4ec2-b711c3a7c29d/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.pl.iso": [
            "https://winworldpc.com/download/c04d43f7-bf3d-11e8-9b71-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c04d43f7-bf3d-11e8-9b71-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.pt-br.iso": [
            "https://winworldpc.com/download/c01502e4-2682-11e9-9b71-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c01502e4-2682-11e9-9b71-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.sk.iso": [
            "https://winworldpc.com/download/a70137b3-a596-11e9-b7f9-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/a70137b3-a596-11e9-b7f9-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ],
        "windows-98-se-oem.zh-tw.iso": [
            "https://winworldpc.com/download/c3b3c3a7-7f0d-440c-11c3-a7c29d255254/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ]
    }

    officeIsoUrlDict = {
        "office-97-standard.iso": {
            "https://winworldpc.com/download/c80848c9-5330-11e9-8581-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/c80848c9-5330-11e9-8581-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        },
        "office-97-professional.iso": {
            "https://winworldpc.com/download/c38d7046-3dc3-a312-11c3-a5c28f135254/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2"
            "https://winworldpc.com/download/c38d7046-3dc3-a312-11c3-a5c28f135254/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        },
        "office-97-small-business.iso": {
            "https://winworldpc.com/download/45c3aa7c-3518-c39a-11c3-a4e284a2c3a5/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/45c3aa7c-3518-c39a-11c3-a4e284a2c3a5/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        },
        "office-97-sr2.iso": {
            "https://winworldpc.com/download/ae075b98-431f-11eb-b764-0200008a0da4/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/ae075b98-431f-11eb-b764-0200008a0da4/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        },
        "office-97-dev-tools.iso": {
            "https://winworldpc.com/download/120dc0ab-5832-11e9-9db4-fa163e9022f0/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
            "https://winworldpc.com/download/120dc0ab-5832-11e9-9db4-fa163e9022f0/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
        },
        "office-97-dev-resourcelib.iso": {
            "https://winworldpc.com/download/aa925d7c-f4bb-11ec-8dc3-0200008a0da4/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/aa925d7c-f4bb-11ec-8dc3-0200008a0da4/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        },
    }

    @classmethod
    def downloadWin98(cls, dstDir, funcMapKey, funcMapValue):
        Util.download("win98iso.7z", cls.winIsoUrlDict[funcMapKey], "win98iso.7z")

        subprocess.check_call(["7z", "x", "-y", "win98iso.7z"])

        ret = glob.glob("**/*.iso", recursive=True)
        if len(ret) == 0:
            ret = glob.glob("**/*.ISO", recursive=True)
            if len(ret) == 0:
                raise Exception("win98 iso not found")
        if len(ret) > 1:
            raise Exception("multiple win98 iso not found")
        ret = ret[0]

        os.chmod(ret, 0o644)
        Util.forceMove(ret, os.path.join(dstDir, funcMapKey))

    @staticmethod
    def downloadBootFloppy98(dstDir, funcMapKey, funcMayValue):
        fn = "Microsoft Windows 98 Second Edition - Boot Disk (3.5-1.44mb).7z"
        urls = [
            "https://winworldpc.com/download/49c380c2-a9c3-af25-c389-11c3a6e28094/from/c39ac2af-c381-c2bf-1b25-11c3a4e284a2",
            "https://winworldpc.com/download/49c380c2-a9c3-af25-c389-11c3a6e28094/from/c3ae6ee2-8099-713d-3411-c3a6e280947e",
        ]
        Util.download("bfloppy98", urls, fn)

        subprocess.check_call(["7z", "x", fn], stdout=subprocess.DEVNULL)
        Util.forceMove(os.path.join("Microsoft Windows 98 Second Edition - Boot Disk (3.5-1.44mb)", "Windows 98 Second Edition Boot.img"), os.path.join(dstDir, funcMapKey))

    @staticmethod
    def downloadDp98(dstDir, funcMapKey, funcMapValue):
        # the original page is not downloadable anymore
        # extracts target files from soporific's iso, https://msfn.org/board/topic/49892-unattended-and-updated-boot-cd-for-windows-98/

        userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36"
        for i in range(1, 8):
            fn = "Windows98.UBCD.Feb.2008.part%d.rar" % (i)
            url = os.path.join("https://www.mdgx.com/ubcd/%s" % (fn))
            Util.download(fn, [url], fn, userAgent=userAgent)           # www.mdgx.com bans wget

        unrardll.extract("Windows98.UBCD.Feb.2008.part1.rar", ".")      # note: 7z can not extract rar file

        Util.extractFromIso("Windows98.UBCD.Feb.2008.[sharethefiles.com].iso", {
            "/drivers/packs/DP_w98_1.01.exe": "DP_w98_1.01.exe",
            "/drivers/packs/DP_w98_2.01.exe": "DP_w98_2.01.exe",
            "/drivers/packs/DP_w98_3.01.exe": "DP_w98_3.01.exe",
            "/drivers/packs/DP_w98_4.02.exe": "DP_w98_4.02.exe",
            "/drivers/packs/DP_w98_5.01.exe": "DP_w98_5.01.exe",
            "/drivers/packs/DP_w98_6.01.exe": "DP_w98_6.01.exe",
            "/drivers/packs/DP_w98_7.01.exe": "DP_w98_7.01.exe",
        })

        Util.rar2zip("DP_w98_1.01.exe", os.path.join(dstDir, "DP_w98_1.01.zip"), "./tmp")
        Util.rar2zip("DP_w98_2.01.exe", os.path.join(dstDir, "DP_w98_2.01.zip"), "./tmp")
        Util.rar2zip("DP_w98_3.01.exe", os.path.join(dstDir, "DP_w98_3.01.zip"), "./tmp")
        Util.rar2zip("DP_w98_4.02.exe", os.path.join(dstDir, "DP_w98_4.02.zip"), "./tmp")
        Util.rar2zip("DP_w98_5.01.exe", os.path.join(dstDir, "DP_w98_5.01.zip"), "./tmp")
        Util.rar2zip("DP_w98_6.01.exe", os.path.join(dstDir, "DP_w98_6.01.zip"), "./tmp")
        Util.rar2zip("DP_w98_7.01.exe", os.path.join(dstDir, "DP_w98_7.01.zip"), "./tmp")

    @classmethod
    def downloadOffice97(cls, dstDir, funcMapKey, funcMapValue):
        Util.download("office97iso.7z", cls.officeIsoUrlDict[funcMapKey], "office97iso.7z")

        subprocess.check_call(["7z", "x", "-y", "office97iso.7z"])

        ret = glob.glob("**/*.iso", recursive=True)
        if len(ret) == 0:
            ret = glob.glob("**/*.ISO", recursive=True)
            if len(ret) == 0:
                raise Exception("office97 iso not found")
        if len(ret) > 1:
            raise Exception("multiple office97 iso not found")
        ret = ret[0]

        os.chmod(ret, 0o644)
        Util.forceMove(ret, os.path.join(dstDir, funcMapKey))


class TorrentDownloader:

    def __init__(self):
        self._torrentDict = {}
        self._session = None

    def addTorrent(self, name, torrentFilePath, torrentCfgFilePath, tmpDir):
        assert self._session is None
        ti = torrent_info(torrentFilePath)
        self._torrentDict[ti.name()] = (torrentFilePath, torrentCfgFilePath, tmpDir, ti, name)

    def startDownload(self):
        assert self._session is None

        self._session = torrent_session()
        try:
            # change settings
            settings = self._session.get_settings()
            settings["active_downloads"] = -1
            settings["active_seeds"] = -1
            self._session.apply_settings(settings)

            # add torrents
            for torrentFilePath, torrentCfgFilePath, tmpDir, ti, name in self._torrentDict.values():
                self._session.add_torrent({
                    'ti': ti,
                    'save_path': tmpDir,
                })
        except BaseException:
            self._session = None        # FIXME: destroy session?
            raise

    def pollCompleteTorrent(self):
        assert self._session is not None

        if len(self._session.get_torrents()) == 0:
            return None

        completeList = []
        statusDict = {}

        for handle in self._session.get_torrents():
            torrentFilePath, torrentCfgFilePath, tmpDir, ti, name = self._torrentDict[handle.torrent_file().name()]
            s = handle.status()
            if not s.is_finished:
                statusDict[name] = s
            else:
                Util.deleteRedundantFilesFromTorrentResultDir(tmpDir, handle=handle)
                self._session.remove_torrent(handle)
                completeList.append(name)

        if len(completeList) > 0:
            return (True, completeList)
        else:
            return (False, statusDict)

    def dispose(self):
        self._session = None            # FIXME: destroy session?
        self._torrentDict = {}


class TempChdir:

    def __init__(self, dirname):
        self.olddir = os.getcwd()
        os.chdir(dirname)

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        os.chdir(self.olddir)


class Util:

    @staticmethod
    def generate_sha256(file):
        sha256 = hashlib.sha256()
        with open(file, 'rb') as f:
            while True:
                data = f.read(65536)
                if not data:
                    break
                sha256.update(data)
        return sha256.hexdigest()

    @staticmethod
    def download(identifier, urls, file, fileSize=None, fileHash=None, userAgent=None):
        if os.path.exists(file):
            if fileSize is None or os.path.getsize(file) == fileSize:
                if fileHash is None or Util.generate_sha256(file) == fileHash:
                    # already downloaded
                    return

        tmpFile = file + ".__download__"

        if userAgent is not None:
            userAgent = ["--user-agent=\"%s\"" % (userAgent)]
        else:
            userAgent = []

        if fileSize is None or (os.path.exists(tmpFile) and os.path.getsize(tmpFile) >= fileSize):
            # re-download needed
            continueArg = ["-c"]
        else:
            continueArg = []

        # FIXME
        subprocess.check_call(["wget", *userAgent, "--show-progress", *continueArg, "-O", tmpFile, urls[0]])

        if fileHash is not None and Util.generate_sha256(tmpFile) != fileHash:
            raise Exception(f"invalid {identifier} from source")

        os.rename(tmpFile, file)

    @staticmethod
    def forceDelete(path):
        if os.path.islink(path):
            os.remove(path)
        elif os.path.isfile(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path)
        elif os.path.lexists(path):
            os.remove(path)             # other type of file, such as device node
        else:
            pass                        # path does not exist, do nothing

    @staticmethod
    def forceMove(srcPath, dstPath):
        Util.forceDelete(dstPath)                # FIXME: needed?
        shutil.move(srcPath, dstPath)

    @staticmethod
    def forceMakeEmptyDir(path):
        if not os.path.exists(path):
            os.mkdir(path)
        elif os.path.islink(path) or not os.path.isdir(path):
            Util.forceDelete(path)
            os.mkdir(path)
        else:
            for fn in os.listdir(path):
                Util.forceDelete(os.path.join(path, fn))

    @staticmethod
    def deleteRedundantFilesFromTorrentResultDir(resultDir, handle=None, torrent_path=None, torrent_cfg_path=None):
        if handle is not None:
            assert torrent_path is None and torrent_cfg_path is None
            ti = torrent_handle.torrent_file(handle)
        else:
            assert torrent_path is not None
            ti = torrent_info(torrent_path)

        # remove files that are not in torrent
        pathList = [x.path for x in torrent_info.files(ti)]
        for dirpath, dirnames, filenames in os.walk(resultDir):
            for filename in filenames:
                fullfn = os.path.join(dirpath, filename)
                if Util.removePathPrefix(fullfn, resultDir) not in pathList:
                    os.remove(fullfn)

        # remove empty directores
        for dirpath, dirnames, filenames in os.walk(resultDir):
            for dirname in dirnames:
                fullfn = os.path.join(dirpath, dirname)
                if len(os.listdir(fullfn)) == 0:
                    os.rmdir(fullfn)

    @staticmethod
    def removePathPrefix(fullfn, prefix):
        assert fullfn[:len(prefix + "/")] == prefix + "/"
        return fullfn[len(prefix + "/"):]

    @staticmethod
    def extractFromIso(isoPath, fileInfoDict):
        with open(isoPath, 'rb') as stream:
            iso = pycdlib.PyCdlib()
            try:
                iso.open_fp(stream)

                if iso.has_udf():
                    facade = iso.get_udf_facade()
                elif iso.has_joliet():
                    facade = iso.get_joliet_facade()
                elif iso.has_rock_ridge():
                    facade = iso.get_rock_ridge_facade()
                else:
                    facade = iso.get_iso9660_facade()

                for isoFullfn, destFullfn in fileInfoDict.items():
                    with open(destFullfn, 'wb') as f:
                        facade.get_file_from_iso_fp(f, isoFullfn)
            finally:
                iso.close()

    @staticmethod
    def rar2zip(rarFilePath, zipFilePath, tmpDir):
        Util.forceMakeEmptyDir(tmpDir)

        unrardll.extract(rarFilePath, tmpDir)

        tmpFile = zipFilePath + ".__temp__"
        with zipfile.ZipFile(tmpFile, 'w') as zipf:
            for cur, dirs, files in os.walk(tmpDir):
                for fn in files:
                    fullfn = os.path.join(cur, fn)
                    zipf.write(fullfn, os.path.relpath(fullfn, tmpDir))
        os.rename(tmpFile, zipFilePath)

    @staticmethod
    def validateFile(filePath, fileSize):
        if not os.path.exists(filePath):
            return False

        if fileSize is not None:
            if os.path.getsize(filePath) != fileSize:
                return False

        if filePath.endswith(".zip"):
            with zipfile.ZipFile(filePath, 'r') as zipf:
                if zipf.testzip() is not None:
                    return False

        return True


###############################################################################

if __name__ == "__main__":
    Main().run()
